Цель исследования: выделить наиболее перспективный сегмент пользователей, который станет целевой аудиторией для развития приложения.
Основные задачи:
Описание данных:
В датасете содержатся данные пользователей, впервые совершивших действия в приложении после 7 октября 2019 года.
Структура исследования:
Предобработка данных
1.1. Общая информация о данных в датасетах
1.2. Обработка пропусков
1.3. Приведение данных к необходимым типам
1.4. Приведение наименований стобцов и данных к единому виду
1.5. Обработка дубликаов
1.6. Объединение датасетов
1.7. ВЫВОД (промежуточный)
Определение поведения пльзователей (исследовательский анализ)
2.1. Профили пользователей
2.2. Удержание пользователей (retention rate)
2.3. Время, проведенное пользователями в приложении
2.3.1. Выделение сессий
2.3.2. Определение времени сессий
2.4. Частота совершения событий пользователями
2.5. Конверсия пользователей в целевое событие (открытие контактов - contacts_show)
2.6. ВЫВОД (промежуточный)
Сегментация
3.1. Выбор признака сегментации и разделение на группы
3.2. Удержание пользовтелей по сегментам
3.3. Конверсия пользователей в целевое событие по сегментам
3.4. ВЫВОД (промежуточный)
Проверка статсистических гипотез
4.1. Гипотеза 1 - группа пользователей, установивших приложение по ссылке yandex и по ссылке из google демонстрируют разную конверсию в просмотры контактов
4.2. Гипотеза 2 - пользователи, переходящие в рекомендованные объявления, и пользователи, игнорирующие рекомендации демонстрируют разную конверсию в просмотры контактов
4.3. ВЫВОД по статистическим гипотезам
Также необходимо сформировать:
#Импорт необходимых библиотек
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from matplotlib import pyplot as plt
import seaborn as sns
import plotly.express as px
from plotly import graph_objects as go
import scipy.stats as stats
from scipy import stats as st
import math as mth
import warnings
warnings.filterwarnings('ignore')
# загрузка данных
try:
mobile_sourсes, mobile_dataset = (
pd.read_csv('mobile_sourсes.csv'),
pd.read_csv('mobile_dataset.csv')
)
except FileNotFoundError:
print('Укажи верный путь к файлу')
# вывод таблиц на экран
display (mobile_dataset.head())
mobile_sourсes.head()
| event.time | event.name | user.id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| userId | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
| 2 | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | yandex |
| 3 | d9b06b47-0f36-419b-bbb0-3533e582a6cb | other |
| 4 | f32e1e2a-3027-4693-b793-b7b3ff274439 |
# общая информация
mobile_dataset.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event.time 74197 non-null object 1 event.name 74197 non-null object 2 user.id 74197 non-null object dtypes: object(3) memory usage: 1.7+ MB
mobile_sourсes.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 4293 entries, 0 to 4292 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 userId 4293 non-null object 1 source 4293 non-null object dtypes: object(2) memory usage: 67.2+ KB
mobile_sourсes['source'].nunique()
3
mobile_sourсes['userId'].nunique()
4293
Вывод:
По общей информации было видно, что пропуски отсутсвуют, но убедимся в этом следующим способом:
!pip install missingno
import missingno as msno
msno.bar(mobile_dataset);
Requirement already satisfied: missingno in c:\users\naste\anaconda3\lib\site-packages (0.5.2) Requirement already satisfied: matplotlib in c:\users\naste\anaconda3\lib\site-packages (from missingno) (3.7.0) Requirement already satisfied: numpy in c:\users\naste\anaconda3\lib\site-packages (from missingno) (1.23.5) Requirement already satisfied: scipy in c:\users\naste\anaconda3\lib\site-packages (from missingno) (1.10.0) Requirement already satisfied: seaborn in c:\users\naste\anaconda3\lib\site-packages (from missingno) (0.12.2) Requirement already satisfied: contourpy>=1.0.1 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (1.0.5) Requirement already satisfied: python-dateutil>=2.7 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (2.8.2) Requirement already satisfied: packaging>=20.0 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (22.0) Requirement already satisfied: cycler>=0.10 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (0.11.0) Requirement already satisfied: pyparsing>=2.3.1 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (3.0.9) Requirement already satisfied: fonttools>=4.22.0 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (4.25.0) Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (1.4.4) Requirement already satisfied: pillow>=6.2.0 in c:\users\naste\anaconda3\lib\site-packages (from matplotlib->missingno) (9.4.0) Requirement already satisfied: pandas>=0.25 in c:\users\naste\anaconda3\lib\site-packages (from seaborn->missingno) (1.5.3) Requirement already satisfied: pytz>=2020.1 in c:\users\naste\anaconda3\lib\site-packages (from pandas>=0.25->seaborn->missingno) (2022.7) Requirement already satisfied: six>=1.5 in c:\users\naste\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib->missingno) (1.16.0)
Вывод: в датасетах нет пропусков
# приведение дат к типо datetime
mobile_dataset['event.time'] = pd.to_datetime(mobile_dataset['event.time']).round('S')
mobile_dataset.head(2)
| event.time | event.name | user.id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
mobile_dataset.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event.time 74197 non-null datetime64[ns] 1 event.name 74197 non-null object 2 user.id 74197 non-null object dtypes: datetime64[ns](1), object(2) memory usage: 1.7+ MB
mobile_dataset.columns = mobile_dataset.columns.str.replace('.', '_')
mobile_dataset.head(2)
| event_time | event_name | user_id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
mobile_sourсes = mobile_sourсes.rename(columns = {'userId':'user_id'})
mobile_sourсes.head(2)
| user_id | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
# проверка на явные дубликаты
display(mobile_dataset.duplicated().sum())
mobile_sourсes.duplicated().sum()
1118
0
# процент дубликатов
x = mobile_dataset.duplicated().sum()/mobile_dataset['event_time'].count()
f'Процент дубликатов в датасете mobile_dataset: {x:.2%}'
'Процент дубликатов в датасете mobile_dataset: 1.51%'
Процент дубликатов мал, знаит можно удалить явные дубликаты из датасета
mobile_dataset = mobile_dataset.drop_duplicates()
mobile_dataset.duplicated().sum()
0
mobile_dataset.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 73079 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 73079 non-null datetime64[ns] 1 event_name 73079 non-null object 2 user_id 73079 non-null object dtypes: datetime64[ns](1), object(2) memory usage: 2.2+ MB
Вывод: после удаления явных дубликатов в датасете осталось 73 079 событий
#просмотр уникальных значений событий
mobile_dataset['event_name'].unique()
array(['advert_open', 'tips_show', 'map', 'contacts_show', 'search_4',
'search_5', 'tips_click', 'photos_show', 'search_1', 'search_2',
'search_3', 'favorites_add', 'contacts_call', 'search_6',
'search_7', 'show_contacts'], dtype=object)
Так как contacts_show и show_contacts являются одним событием, то необходимо привети их к одному наименованию.
Также можно все события связанные с поиском назвать одинаково.
# объединение contacts_show и show_contacts
mobile_dataset_clean = mobile_dataset
mobile_dataset_clean['event_name'] = mobile_dataset_clean['event_name'].replace('contacts_show', 'show_contacts')
for s in ['search_1','search_2', 'search_3', 'search_4','search_5', 'search_6', 'search_7']:
mobile_dataset_clean['event_name'] = mobile_dataset_clean['event_name'].replace(s, 'search')
mobile_dataset_clean['event_name'].unique()
array(['advert_open', 'tips_show', 'map', 'show_contacts', 'search',
'tips_click', 'photos_show', 'favorites_add', 'contacts_call'],
dtype=object)
mobile_dataset_clean.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 73079 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 73079 non-null datetime64[ns] 1 event_name 73079 non-null object 2 user_id 73079 non-null object dtypes: datetime64[ns](1), object(2) memory usage: 2.2+ MB
data = mobile_dataset_clean.merge(mobile_sourсes, how='outer')
# добавление столбца с неделями
data['event_week'] = data['event_time'].astype('datetime64[W]')
data
| event_time | event_name | user_id | source | event_week | |
|---|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:00:00 |
| 1 | 2019-10-07 00:00:01 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:00:01 |
| 2 | 2019-10-07 00:00:07 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:00:07 |
| 3 | 2019-10-07 00:01:28 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:01:28 |
| 4 | 2019-10-07 00:01:35 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:01:35 |
| ... | ... | ... | ... | ... | ... |
| 73074 | 2019-11-03 23:46:47 | map | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 23:46:47 | |
| 73075 | 2019-11-03 23:46:59 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 23:46:59 | |
| 73076 | 2019-11-03 23:47:01 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 23:47:01 | |
| 73077 | 2019-11-03 23:47:47 | advert_open | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 23:47:47 | |
| 73078 | 2019-11-03 23:47:50 | tips_show | d157bffc-264d-4464-8220-1cc0c42f43a9 | 2019-11-03 23:47:50 |
73079 rows × 5 columns
data.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 73079 entries, 0 to 73078 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 73079 non-null datetime64[ns] 1 event_name 73079 non-null object 2 user_id 73079 non-null object 3 source 73079 non-null object 4 event_week 73079 non-null datetime64[ns] dtypes: datetime64[ns](2), object(3) memory usage: 3.3+ MB
Вывод: в объединенном датасете сохраниось 73 079 событий, это значит, что в датасете с источниками установок не было "лишних" пользователей, не совершивших никаких событий.
По резульатам предобработки данных:
def get_profiles(events):
"""
Эта функция определяет профили пользователей.
На вход функция принимает общий датасет с событиями.
На выходе функция возвращает перофили пользователей, включающие их первое событие,
наименование этого события, источник установки и дату события
"""
profiles = (
events.sort_values(by=['user_id', 'event_time'])
.groupby('user_id')
.agg({'event_time': 'first', 'event_name':'first','source': 'first'})
.rename(columns={'event_time': 'first_ts'})
.reset_index() # возвращаем user_id из индекса
)
profiles['dt'] = profiles['first_ts'].dt.date
profiles['week'] = profiles['first_ts'].astype('datetime64[W]')
return profiles
profiles = get_profiles(data)
profiles.head()
| user_id | first_ts | event_name | source | dt | week | |
|---|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:39:46 | tips_show | other | 2019-10-07 | 2019-10-07 13:39:46 |
| 1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:34:34 | search | yandex | 2019-10-19 | 2019-10-19 21:34:34 |
| 2 | 00463033-5717-4bf1-91b4-09183923b9df | 2019-11-01 13:54:35 | photos_show | yandex | 2019-11-01 | 2019-11-01 13:54:35 |
| 3 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-18 22:14:06 | search | 2019-10-18 | 2019-10-18 22:14:06 | |
| 4 | 00551e79-152e-4441-9cf7-565d7eb04090 | 2019-10-25 16:44:41 | show_contacts | yandex | 2019-10-25 | 2019-10-25 16:44:41 |
# определение минимальной даты первого события
first_ts_min = profiles['first_ts'].min()
first_ts_min
Timestamp('2019-10-07 00:00:00')
# определение максимальной даты первого события
first_ts_max = profiles['first_ts'].max()
first_ts_max
Timestamp('2019-11-03 23:46:47')
Вывод:
# функция для расчёта удержания
def get_retention(
profiles, sessions
):
'''
Данная функция определяет удержание пользователей по неделям
Функция принимает на вход общий датасет с событиями
и датасет профилей пользоватлей.
На выходе функция возвращает таблицу с данными из профилей, с неделей,
когда было соврешено событие и лайфтайм в днях (количество дней кратно 7-ми)
'''
# сбор сырых данных
result_raw = profiles.merge(
sessions[['user_id', 'event_week']], on='user_id', how='left'
)
result_raw['lifetime'] = (
result_raw['event_week'] - result_raw['week']
).dt.days
# рассчитет удержание
result_grouped = result_raw.pivot_table(
index=['dt'], columns='lifetime', values='user_id', aggfunc='nunique'
)
cohort_sizes = (
result_raw.groupby('dt')
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result_grouped = cohort_sizes.merge(
result_grouped, on='dt', how='left'
).fillna(0)
result_grouped = result_grouped.div(result_grouped['cohort_size'], axis=0)
# восстанавление столбеца с размерами когорт
result_grouped['cohort_size'] = cohort_sizes
# возвращаем таблицу удержания и сырые данные
return result_raw, result_grouped
# получаем сырые данные и готовую таблицу
retention_raw, retention_rate = get_retention(profiles, data)
# функция построения хитмэпа удержания
def heatmap(retention):
#plt.figure(figsize=(15, 15))
sns.heatmap(
retention.drop(columns=['cohort_size', 0]),
annot=True,
fmt='.2%',
)
plt.title('Тепловая карта удержания')
plt.xlabel('Лайфтайм')
plt.ylabel('Дата первого действия пользователей')
plt.show()
# переименование столбцов недель для удобства
retention_rate = retention_rate.rename(columns={7: '1st week', 14: '2nd week', 21: '3d week', 28: '4th week'})
# построение
plt.figure(figsize=(15, 15))
heatmap(retention_rate)
Вывод:
#подготовка датасета к формированию сессий - сортировка
data_sorted = data.sort_values(['user_id', 'event_time'])
# определение медианной разницы между временем соседних событий
delta = data_sorted.groupby('user_id')['event_time'].diff()
delta.median()
Timedelta('0 days 00:01:12')
Выше была рассчитана разница между временем соседних событий. Рассчитав медианное значение для распределения полученной дельты можно предположить, что большинство новых действий начинается примерно через 1 час 12 минут после начала предыдущих. Округлим это значение до 1 часа. То есть для разделения на сессии за дельту можно принять дельту 60 минут
# определение разницы больше 60 минут для каждой группы с кумулятивной суммой
g = (delta > pd.Timedelta('60Min')).cumsum()
# создание счетчика для групп
data_sorted['session_id'] = data_sorted.groupby(['user_id', g], sort=False).ngroup() + 1
data_sorted['time_delta'] = delta
data_sorted = data_sorted.reset_index(drop=True)
data_sorted
| event_time | event_name | user_id | source | event_week | session_id | time_delta | |
|---|---|---|---|---|---|---|---|
| 0 | 2019-10-07 13:39:46 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | other | 2019-10-07 13:39:46 | 1 | NaT |
| 1 | 2019-10-07 13:40:31 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | other | 2019-10-07 13:40:31 | 1 | 0 days 00:00:45 |
| 2 | 2019-10-07 13:41:06 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | other | 2019-10-07 13:41:06 | 1 | 0 days 00:00:35 |
| 3 | 2019-10-07 13:43:21 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | other | 2019-10-07 13:43:21 | 1 | 0 days 00:02:15 |
| 4 | 2019-10-07 13:45:31 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | other | 2019-10-07 13:45:31 | 1 | 0 days 00:02:10 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 73074 | 2019-11-03 15:51:24 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 15:51:24 | 9573 | 0 days 00:00:28 | |
| 73075 | 2019-11-03 15:51:58 | show_contacts | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 15:51:58 | 9573 | 0 days 00:00:34 | |
| 73076 | 2019-11-03 16:07:41 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 16:07:41 | 9573 | 0 days 00:15:43 | |
| 73077 | 2019-11-03 16:08:18 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 16:08:18 | 9573 | 0 days 00:00:37 | |
| 73078 | 2019-11-03 16:08:25 | tips_show | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | 2019-11-03 16:08:25 | 9573 | 0 days 00:00:07 |
73079 rows × 7 columns
Вывод:
#Определение времени каждой сессии
sessions_time = data_sorted.groupby(['session_id']).agg({'event_time':['min','max']}).rename(columns = {'min':'start_time', 'max':'finish_time'}).reset_index()
sessions_time['time'] = sessions_time['event_time', 'finish_time']-sessions_time['event_time', 'start_time']
sessions_time['minutes'] = round(sessions_time['time'].dt.seconds/60,2)
sessions_time
| session_id | event_time | time | minutes | ||
|---|---|---|---|---|---|
| start_time | finish_time | ||||
| 0 | 1 | 2019-10-07 13:39:46 | 2019-10-07 13:49:42 | 0 days 00:09:56 | 9.93 |
| 1 | 2 | 2019-10-09 18:33:56 | 2019-10-09 18:42:23 | 0 days 00:08:27 | 8.45 |
| 2 | 3 | 2019-10-21 19:52:31 | 2019-10-21 20:07:30 | 0 days 00:14:59 | 14.98 |
| 3 | 4 | 2019-10-22 11:18:15 | 2019-10-22 11:30:53 | 0 days 00:12:38 | 12.63 |
| 4 | 5 | 2019-10-19 21:34:34 | 2019-10-19 21:59:55 | 0 days 00:25:21 | 25.35 |
| ... | ... | ... | ... | ... | ... |
| 9568 | 9569 | 2019-11-01 00:24:31 | 2019-11-01 00:24:53 | 0 days 00:00:22 | 0.37 |
| 9569 | 9570 | 2019-11-02 01:16:49 | 2019-11-02 01:16:49 | 0 days 00:00:00 | 0.00 |
| 9570 | 9571 | 2019-11-02 18:01:27 | 2019-11-02 18:17:41 | 0 days 00:16:14 | 16.23 |
| 9571 | 9572 | 2019-11-02 19:25:54 | 2019-11-02 19:30:50 | 0 days 00:04:56 | 4.93 |
| 9572 | 9573 | 2019-11-03 14:32:56 | 2019-11-03 16:08:25 | 0 days 01:35:29 | 95.48 |
9573 rows × 5 columns
#построение боксплота распределения времени сессии
df = px.data.tips()
fig = px.box(sessions_time, y="minutes", points="all")
fig.update_layout(title='Распределение сессий по времени',
yaxis_title='Время сессии, минуты'
)
fig.show()
#характеристики распределения времени сессий
sessions_time['time'].describe()
count 9573 mean 0 days 00:17:30.913715658 std 0 days 00:28:32.457677489 min 0 days 00:00:00 25% 0 days 00:00:44 50% 0 days 00:06:49 75% 0 days 00:21:45 max 0 days 07:14:59 Name: time, dtype: object
На графике распределения времени сессий видно, что длинные сессии это явные аномалии (пользователь не вышел из приложения и оно автоматически не закрылось, программынй сбой в сборе данных, приложение зависло, а запись сессии продолжалась и т.п.). Но значений близких к 0 много. В связи с этим посмотрим на количество нулевых сессиий и попробуем предположить их происхождение.
#расчет количества нулевых сессий
sessions_0 = sessions_time[sessions_time['time'] == '0 days 00:00:00']
sessions_0['session_id'].count()
1780
#выборка нулевых сессий
sessions_0_id = sessions_0['session_id'].unique()
# просмотр событий в нулевых сессиях
data_sorted.query('session_id in @sessions_0_id').groupby('event_name').agg({'event_name':'count'})
| event_name | |
|---|---|
| event_name | |
| advert_open | 40 |
| favorites_add | 21 |
| map | 82 |
| photos_show | 467 |
| search | 340 |
| show_contacts | 107 |
| tips_click | 7 |
| tips_show | 716 |
Вывод:
# вывод общего распределения событий
# группировка по событиям
data_events = data.groupby('event_name').agg({'event_name':'count'}).rename(columns = {'event_name':'amount_of_events'}).sort_values(by='amount_of_events').reset_index()
# столбчатая диаграмма по событиям
fig = px.bar(data_events, x='event_name', y='amount_of_events', title='Общее количество событий по типам', text='amount_of_events')
fig.update_xaxes(title_text='Название события')
fig.update_yaxes(title_text='Количество событий')
fig.update_layout(width=600, height=600)
fig.update_traces(hoverinfo="all", hovertemplate="Название события: %{x}<br>Количество совершенных событий: %{y}")
fig.show()
# количество совершенных событий по типам от общего числа событий
fig = go.Figure(data=[go.Pie(labels=data_events['event_name'], values=data_events['amount_of_events'])])
fig.update_layout(title='Распределение количетсва событий',
width=600,
height=400,
annotations=[dict(x=1.33,
y=1.10,
text='Событие',
showarrow=False)])
fig.show()
# построекние гистограмы числа событий по дате и времени
data['event_time'].hist(bins=120, figsize = (20,10))
plt.title('Распределение числа событий по времени')
plt.xlabel('Дата')
plt.ylabel('Количество событий');
#распределение событий по дням и по названиям
data['dt'] = data['event_time'].dt.date
fig = px.histogram(data,
x='dt',
color='event_name'
)
fig.update_layout(title='Распределение событий по времени',
width=1000,
height=1000,
xaxis_title='Дата',
yaxis_title='Количество событий',
bargap=0.2
)
fig.show()
# просмотр распределения количества событий в течение дня для самого "активного" дня - 23 октября
data_23oct = data[data['dt'] == pd.to_datetime('2019-10-23')]
fig = px.histogram(data_23oct,
x='event_time',
#color='event_name'
)
fig.update_layout(title='Распределение событий по времени 23 октября 2019 года',
width=800,
height=600,
xaxis_title='Время',
yaxis_title='Количество событий',
bargap=0.2
)
fig.show()
#добавление к датасету дня недели совершения события
data['weekday'] = data['event_time'].dt.day_name ()
data_events_weekday = data.groupby(['weekday','event_name']).agg({'event_name':'count'}).rename(columns = {'event_name':'amount'}).sort_values(by='amount', ascending = False).reset_index()
#распределение событий по дням недели и по названиям
fig = px.bar(data_events_weekday.sort_values(by = 'amount', ascending = False),
x='weekday',
y='amount',
color='event_name',
text = 'amount'
)
fig.update_layout(title='Распределение событий по дням недели',
width=800,
height=600,
xaxis_title='День недели',
yaxis_title='Количество событий'
)
fig.show()
#общее распределение по дням недели
data_events_weekday_short = data.groupby(['weekday']).agg({'event_name':'count'}).rename(columns = {'event_name':'amount'}).sort_values(by='amount', ascending = False).reset_index()
fig = px.bar(data_events_weekday_short.sort_values(by = 'amount', ascending = False),
x='weekday',
y='amount',
text = 'amount'
)
fig.update_layout(title='Распределение событий по дням недели',
width=800,
height=300,
xaxis_title='День недели',
yaxis_title='Количество событий'
)
fig.show()
Выводы:
# расчет конверсии относительно общего числа пришедших пользователей
convertion = (data[data['event_name'] == 'show_contacts']['user_id'].nunique())/(data['user_id'].nunique())
f'Конверсия пользователей в целевое событие "просмотр контактов": {convertion:.2%}'
'Конверсия пользователей в целевое событие "просмотр контактов": 22.85%'
Вывод: 22,85% процентов пользователей доходят до целевого события просмостра контактов
По результатам рассмотрения общего повдения пользоватлей приложения:
На основе авктивности по дням недели предлагается сегментация по признаку: в какой день недели пользователь совершил первое действие (т.е. пришел в приложение).
Понимание, в какой день недели приходят наиболее преспективные пользователи позволит повысить эффективность рекламных кампаний (можно будет запускать рекламу и акции в определенный день недели), усилить активность генерации рекомендаций в определенные дни, давать рекомендации продавцам на основе внутренней аналитики.
На первый взгляд можно предположить, что наиболее привлекательными и перспективными будут пользователи, приходящие в пондельник-вторник и, возможно, в воскресенье, в связи с их высокой активностью.
Для начала посмотрим в приниципе сколько пользователей совершило свое первое действие в каждый день недели.
#добавление в таблицу профилей пользователей столбца с днем недели первого действия
profiles['weekday'] = profiles['first_ts'].dt.day_name ()
profiles['weekday_number'] = profiles['first_ts'].dt.weekday
profiles.head()
| user_id | first_ts | event_name | source | dt | week | weekday | weekday_number | |
|---|---|---|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:39:46 | tips_show | other | 2019-10-07 | 2019-10-07 13:39:46 | Monday | 0 |
| 1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:34:34 | search | yandex | 2019-10-19 | 2019-10-19 21:34:34 | Saturday | 5 |
| 2 | 00463033-5717-4bf1-91b4-09183923b9df | 2019-11-01 13:54:35 | photos_show | yandex | 2019-11-01 | 2019-11-01 13:54:35 | Friday | 4 |
| 3 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-18 22:14:06 | search | 2019-10-18 | 2019-10-18 22:14:06 | Friday | 4 | |
| 4 | 00551e79-152e-4441-9cf7-565d7eb04090 | 2019-10-25 16:44:41 | show_contacts | yandex | 2019-10-25 | 2019-10-25 16:44:41 | Friday | 4 |
#расчет числа пользователей по дням недели
users_per_weekday = profiles.groupby(['weekday_number','weekday']).agg({'user_id':'count'}).reset_index()
fig = px.bar(users_per_weekday,
x='weekday',
y='user_id',
text = 'user_id'
)
fig.update_layout(title='Распределение пользователей по дням недели, в которые они пришли в приложение',
width=800,
height=300,
xaxis_title='День недели',
yaxis_title='Количество пользователей'
)
fig.show()
На основе графика из пункта 3.4 предлагаются следующие группы:
# добавление столбца с номером группы в таблицу профилей
def category(income):
if income <= 1:
return 1
elif income <= 3:
return 2
elif income <= 5:
return 3
elif income == 6:
return 4
profiles['group_id'] = profiles['weekday_number'].apply(category)
profiles.head()
| user_id | first_ts | event_name | source | dt | week | weekday | weekday_number | group_id | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:39:46 | tips_show | other | 2019-10-07 | 2019-10-07 13:39:46 | Monday | 0 | 1 |
| 1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:34:34 | search | yandex | 2019-10-19 | 2019-10-19 21:34:34 | Saturday | 5 | 3 |
| 2 | 00463033-5717-4bf1-91b4-09183923b9df | 2019-11-01 13:54:35 | photos_show | yandex | 2019-11-01 | 2019-11-01 13:54:35 | Friday | 4 | 3 |
| 3 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-18 22:14:06 | search | 2019-10-18 | 2019-10-18 22:14:06 | Friday | 4 | 3 | |
| 4 | 00551e79-152e-4441-9cf7-565d7eb04090 | 2019-10-25 16:44:41 | show_contacts | yandex | 2019-10-25 | 2019-10-25 16:44:41 | Friday | 4 | 3 |
# добавление групп в общий датасет
data_shared = data.merge(profiles[['user_id', 'group_id']], on = 'user_id', how = 'left').sort_values(by = ['group_id', 'event_time'])
data_shared.head()
| event_time | event_name | user_id | source | event_week | dt | weekday | group_id | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2019-10-07 00:00:00 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:00:00 | 2019-10-07 | Monday | 1 |
| 1 | 2019-10-07 00:00:01 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:00:01 | 2019-10-07 | Monday | 1 |
| 28 | 2019-10-07 00:00:02 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex | 2019-10-07 00:00:02 | 2019-10-07 | Monday | 1 |
| 2 | 2019-10-07 00:00:07 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 | other | 2019-10-07 00:00:07 | 2019-10-07 | Monday | 1 |
| 29 | 2019-10-07 00:00:56 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex | 2019-10-07 00:00:56 | 2019-10-07 | Monday | 1 |
# разделение данных на 4 датасета
data_group_1 = data_shared[data_shared['group_id'] == 1].reset_index(drop=True).drop(columns = ['dt', 'weekday', 'group_id'],axis = 1)
data_group_2 = data_shared[data_shared['group_id'] == 2].reset_index(drop=True).drop(columns = ['dt', 'weekday', 'group_id'],axis = 1)
data_group_3 = data_shared[data_shared['group_id'] == 3].reset_index(drop=True).drop(columns = ['dt', 'weekday', 'group_id'],axis = 1)
data_group_4 = data_shared[data_shared['group_id'] == 4].reset_index(drop=True).drop(columns = ['dt', 'weekday', 'group_id'],axis = 1)
profiles_1 = profiles[profiles['group_id'] == 1].reset_index(drop=True)
profiles_2 = profiles[profiles['group_id'] == 2].reset_index(drop=True)
profiles_3 = profiles[profiles['group_id'] == 3].reset_index(drop=True)
profiles_4 = profiles[profiles['group_id'] == 4].reset_index(drop=True)
#проверка размера групп
print(data_group_1['user_id'].nunique())
print(data_group_2['user_id'].nunique())
print(data_group_3['user_id'].nunique())
print(data_group_4['user_id'].nunique())
1353 1303 1071 566
Вывод: было произведено сегментирование пользователей на 4 группы по признаку "день недели, когда было совершено первое действие в приложении".
# удержание пользователей по группам
for i in data_shared['group_id'].unique():
profiles_i = profiles[profiles['group_id'] == i].reset_index(drop=True)
data_group_i = data_shared[data_shared['group_id'] == i].reset_index(drop=True)
retention_raw_i, retention_rate_i = get_retention(profiles_i, data_group_i)
retention_rate_i = retention_rate_i.rename(columns={7: '1st week', 14: '2nd week', 21: '3d week', 28: '4th week'})
print( f'Для группы {i}')
plt.figure(figsize=(10, 5))
heatmap(retention_rate_i)
Для группы 1
Для группы 2
Для группы 3
Для группы 4
Вывод:
# расчет конверсии относительно общего числа пользователей (группа 1)
convertion = (data_group_1[data_group_1['event_name'] == 'show_contacts']['user_id'].nunique())/(data_group_1['user_id'].nunique())
f'Конверсия пользователей в целевое событие "просмотр контактов" для группы 1: {convertion:.2%}'
'Конверсия пользователей в целевое событие "просмотр контактов" для группы 1: 21.88%'
# расчет конверсии относительно общего числа пользователей (группа 2)
convertion = (data_group_2[data_group_2['event_name'] == 'show_contacts']['user_id'].nunique())/(data_group_2['user_id'].nunique())
f'Конверсия пользователей в целевое событие "просмотр контактов" для группы 2: {convertion:.2%}'
'Конверсия пользователей в целевое событие "просмотр контактов" для группы 2: 23.71%'
# расчет конверсии относительно общего числа пользователей (группа 3)
convertion = (data_group_3[data_group_3['event_name'] == 'show_contacts']['user_id'].nunique())/(data_group_2['user_id'].nunique())
f'Конверсия пользователей в целевое событие "просмотр контактов" для группы 3: {convertion:.2%}'
'Конверсия пользователей в целевое событие "просмотр контактов" для группы 3: 19.80%'
# расчет конверсии относительно общего числа пользователей (группа 4)
convertion = (data_group_4[data_group_4['event_name'] == 'show_contacts']['user_id'].nunique())/(data_group_4['user_id'].nunique())
f'Конверсия пользователей в целевое событие "просмотр контактов" для группы 4: {convertion:.2%}'
'Конверсия пользователей в целевое событие "просмотр контактов" для группы 4: 20.85%'
# функция проверки двусторонней гипотезы
def statistic_check(data_total_1,data_total_2, data_success_1, data_success_2, alpha):
customers = np.array([data_total_1['user_id'].nunique(), data_total_2['user_id'].nunique()])
success = np.array([data_success_1['user_id'].nunique(), data_success_2['user_id'].nunique()])
p1 = success[0]/customers[0]
p2 = success[1]/customers[1]
p_combined = (success[0] + success[1]) / (customers[0] + customers[1])
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/customers[0] + 1/customers[1]))
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между группами есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными')
# принимаем альфа за 0,05
alpha = 0.05
Группа пользователей, установивших приложение по ссылке yandex и по ссылке из google демонстрируют разную конверсию в просмотры контактов
Н0: Между группой пользователей, установивших приложение по ссылке yandex, и группой пользователей, установивших приложение по ссылке из google, нет значимой разницы в конверсии в целевое событие "просмотр контактов"
Н1: Между группой пользователей, установивших приложение по ссылке yandex, и группой пользователей, установивших приложение по ссылке из google, есть значимая разница в конверсии в целевое событие "просмотр контактов"
# Выделение группы пользователей, пришедших по ссылке Yandex
data_ya_total = data[data['source'] == 'yandex']
# Выделение группы пользователей, пришедших по ссылке google
data_g_total = data[data['source'] == 'google']
#выделение группы усмпешных событий в группе Yandex
data_ya_success = data_ya_total[data_ya_total['event_name'] == 'show_contacts']
#выделение группы усмпешных событий в группе Yandex
data_g_success = data_g_total[data_g_total['event_name'] == 'show_contacts']
statistic_check(data_ya_total, data_g_total, data_ya_success, data_g_success, alpha)
p-значение: 0.8244316027993777 Не получилось отвергнуть нулевую гипотезу, нет оснований считать группы пользователей разными
Пользователи, переходящие в рекомендованные объявления, и пользователи, игнорирующие рекомендации демонстрируют разную конверсию в просмотры контактов
Н0: Между группой пользователей, переходящих в рекомендованные объявления, и группой пользователей, игнорирующих рекомендации, нет значимой разницы в конверсии в целевое событие "просмотр контактов"
Н1: Между группой пользователей, переходящих в рекомендованные объявления, и группой пользователей, игнорирующих рекомендации, есть значимая разница в конверсии в целевое событие "просмотр контактов"
# Поиск уникальных пользователей, переходящих по рекомендациям
users_use_recom = data[data['event_name'] == 'tips_click']['user_id'].unique()
# Выделение датасета с пользователями, переходящих по рекомендациям
data_recom_total = data.query('user_id in @users_use_recom')
# Выделение группы пользователей, не проходящих по рекомендациям
data_not_recom_total = data.query('user_id not in @users_use_recom')
#выделение группы успешных событий в группе использующих рекомендации
data_recom_success = data_recom_total[data_recom_total['event_name'] == 'show_contacts']
#выделение группы усмпешных событий в группе игнорирующих
data_not_recom_success = data_not_recom_total[data_not_recom_total['event_name'] == 'show_contacts']
# конверсия пользователей, использующих рекомендации
data_recom_success['user_id'].nunique()/data_recom_total['user_id'].nunique()
0.3105590062111801
# конверсия пользователей, не использующих рекомендации
data_not_recom_success['user_id'].nunique()/data_not_recom_total['user_id'].nunique()
0.22185847393603628
statistic_check(data_recom_total, data_not_recom_total, data_recom_success, data_not_recom_success, alpha)
p-значение: 0.00026645646284051416 Отвергаем нулевую гипотезу: между группами есть значимая разница
По результатам статистической проверки гипотез выявлено:
По результатам оценки удержания:
2.1. Максимальный срок «жизни» пользователей составляет 4 недели. Данный промежуток времени не позволяется оценить подробную детализацию удержания пользователей, но позволяет оценить общую картину поведения пользователей в приложении.
2.2. Максимальное удержание на 4ую неделю составляет 8.38%.
2.3. При этом, общая картина (особенно значения удержания пользователей на первой недели) показывает нестабильное поведение пользователей, удержание на 1ую неделю варьируется о 9 до 35% .
2.4. Такая нестабильность может быть обусловлена дополнительными или внешними факторами, о которых мы не знаем (объявление приложением скидок на доставку, добавление нового функционала, который понравился пользователям или нет, проведена рекламная компания, было объявлено о закрытии магазина и теперь покупатели ищут эти товары на вторичном рынке и т.д.)
По результатам оценки времени, проведенного в приложении выявлено:
3.1. Среднестатистический пользователь проводит в приложении около 6 минут за 1 сессию. Не много, но достаточно, чтобы использовать несколько фильтров, найти необходимый предмет и как минимум добавить его в избранное или написать продавцу.
3.2. Самая высокая активность пользователей наблюдается с 14:30 до 14:00 и с 16:00 до 17:00. С учетом типичного времени сессии пользователь вполне могут заглянуть в приложение во время небольшого перерыва в рабочем процессе, обеда или перед уходом из офиса (в зависимости от типичного рабочего дня для наших пользователей). Также активность наблюдается в вечернее время, когда пользователи чаще всего приходят домой с работы.
3.3. По дням недели самая высокая активность пользователей в понедельник, потом постепенно снижается до субботы и в воскресенье вновь увеличивается.
3.4. Показатели того, когда пользователи больше всего заходят в приложение могут помочь выбрать время для запуска обновлений, предложения скидок, выгодных условий и дополнительных рекомендаций пользователям.
3.5. Общая конверсия пользователей в целевое событие «просмотр контактов» составляет 23%, что является достаточно высоким значением, но требуется рассмотрение различных групп, чтобы определить где конверсия не достаточная.
Для более детального анализа были выбран признак: в какой день недели пользователь пришел в приложение (сделал первое действие). По результатам анализа групп:
4.1. Наиболее предсказуемое поведением обладают пользователи, совершающие первое действие в приложении в понедельник или во вторник.
4.2. Наилучшая конверсия пользователей в целевое событие "просмотр контактов" наблюдается для пользователей, совершивших первое действие в среду или четверг (23,71%).
4.3. Однако, стоит порекомендовать «пожертвовать» уровнем конверсии и выбрать целевой группой тех, пользователей, которые приходят в приложение в первой половине недели (можно захватить понедельник, вторник и среду) (для пользователей, приходящих в понедельник и вторник конверсия составляет 21,88%.
4.4. Определение целевой группы позволит увеличить интенсивность рекламы в эти дни, чаще показывать выгодные предложения, также можно подготовить рекомендации для продавцов, в какие дни и время лучше выкладывать объявления о продаже, а также предложить услугу обновления (повышение в выдаче объявлений) в более «выгодные» дни.
По результатам статистической проверки гипотез выявлено:
5.1. Нет оснований считать разницу в конверсии в целевое действие между пользователями, скачавшими приложение по ссылке из Yandex и из Google, значимой.
5.2. То есть скорее всего нет смыла увеличения маркетинговых затрат на ту или иную платформу.
5.3. Между группами пользователей, переходящим по рекомендациям и пользователями игнорирующими рекомендациями есть значимая разница в части конверсии в целевое действие.
5.4. То есть, можно предположить, что рекомендации в приложении действительно эффективны и улучшение данного функционала – это перспективное развитие приложения.